package xmpp.androidpush.service;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.util.Log;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.IqMessage;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.ping.packet.Ping;
import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest;

import java.util.Locale;

import xmpp.androidpush.aidl.PushIqMessage;
import xmpp.androidpush.aidl.PushListener;
import xmpp.androidpush.aidl.PushMessage;
import xmpp.androidpush.util.ConnectionState;
import xmpp.androidpush.util.XmppHelper;

public class SmackableImp implements Smackable {
    final static private String TAG = "xmpp.androidpush.SmackableImp";
    private static final String PING_ALARM = "org.yaxim.androidclient.PING_ALARM";
    private ConnectionConfiguration mXMPPConfig;
    private XmppStreamHandler.ExtXMPPConnection mXMPPConnection;
    private Thread mConnectingThread;
    private Object mConnectingThreadMutex = new Object();
    private ConnectionState mRequestedState;
    private ConnectionState mState;
    private String mLastError;
    private PushListener.PushCallback mServiceCallBack;
    private PacketListener mPacketListener;
    private PacketListener mPresenceListener;
    private PacketListener mIQListener;
    private ConnectionListener mConnectionListener;
    private String mPingID;
    private PendingIntent mPingAlarmPendIntent;
    private Intent mPingAlarmIntent;
    private BroadcastReceiver mPingAlarmReceiver;
    private Service mService;
    private Handler mTimeoutHandler;
    private PushConfig mConfig;

    static {
        SmackConfiguration.setPacketReplyTimeout(30000);
        SmackConfiguration.setKeepAliveInterval(10000);
    }

    public SmackableImp(Service service, PushConfig config) {
        this.mRequestedState = ConnectionState.OFFLINE;
        this.mState = ConnectionState.OFFLINE;
        this.mPingAlarmIntent = new Intent(PING_ALARM);
        this.mPingAlarmReceiver = new SmackableImp.PingAlarmReceiver();
        this.mService = service;
        this.mConfig = config;
        this.mTimeoutHandler = new Handler();
    }

    private synchronized void initXMPPConnection() {
        this.mXMPPConfig = new ConnectionConfiguration(this.mConfig.server, this.mConfig.port, this.mConfig.server);
        this.mXMPPConfig.setReconnectionAllowed(false);
        this.mXMPPConfig.setSendPresence(false);
        this.mXMPPConfig.setCompressionEnabled(false);
        this.mXMPPConfig.setDebuggerEnabled(false);

        if (this.mConfig.require_ssl) {
            this.mXMPPConfig.setSecurityMode(ConnectionConfiguration.SecurityMode.required);
        }

        this.mXMPPConnection = new XmppStreamHandler.ExtXMPPConnection(this.mXMPPConfig);
    }

    public boolean doConnect() throws PushException {
        this.mRequestedState = ConnectionState.ONLINE;
        this.updateConnectionState(ConnectionState.CONNECTING);
        this.setConTimeoutCallback();
        if (this.mXMPPConnection == null) {
            this.initXMPPConnection();
        }

        this.tryToConnect();
        this.mTimeoutHandler.removeCallbacksAndMessages((Object) null);
        if (this.isAuthenticated()) {
            this.registerMessageListener();
            this.registerIQListener();
            this.registerPresenceListener();

            this.registerPongListener();
            this.updateConnectionState(ConnectionState.ONLINE);
            return true;
        } else {
            throw new PushException("SMACK connected, but authentication failed");
        }
    }

    private void setConTimeoutCallback() {
        this.mTimeoutHandler.removeCallbacksAndMessages((Object) null);
        this.mTimeoutHandler.postDelayed(new SmackableImp.TimeoutCallback(), (long) (this.mConfig.connectTimeout * 1000));
    }

    private void updateConnectingThread(Thread new_thread) {
        synchronized (this.mConnectingThreadMutex) {
            if (this.mConnectingThread == null) {
                this.mConnectingThread = new_thread;
            } else {
                try {
                    this.mConnectingThread.interrupt();
                    this.mConnectingThread.join(50L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    this.mConnectingThread = new_thread;
                }
            }

        }
    }

    private void finishConnectingThread() {
        synchronized (this.mConnectingThreadMutex) {
            this.mConnectingThread = null;
        }
    }

    public String getBareJID(String from) {
        String[] res = from.split("/");
        return res[0].toLowerCase(Locale.CHINESE);
    }

    public void setStatusFromConfig(Presence.Mode mode) {
//        CarbonManager.getInstanceFor(this.mXMPPConnection).sendCarbonsEnabled(true);
//        Presence presence = new Presence(Presence.Type.available);
//        presence.setMode(mode);
//        presence.setPriority(125);
//        this.mXMPPConnection.sendPacket(presence);
    }

    @Override
    public XmppStreamHandler.ExtXMPPConnection getConnection() {
        return mXMPPConnection;
    }

    public synchronized void doRequestConnectionState(ConnectionState new_state) {
        Log.e(TAG, "requestConnState: " + mState + " -> " + new_state);

        if (new_state == mState)
            return;
        switch (new_state) {
            case ONLINE:
                switch (mState) {
                    case RECONNECT_DELAYED:
                        // TODO: cancel timer
                    case RECONNECT_NETWORK:
                    case DISCONNECTED:
                    case OFFLINE:
                        // update state before starting thread to prevent race conditions
                        updateConnectionState(ConnectionState.CONNECTING);

                        new Thread() {
                            @Override
                            public void run() {
                                updateConnectingThread(this);
                                try {
                                    doConnect();
                                } catch (IllegalArgumentException e) {
                                    // this might happen when DNS resolution in ConnectionConfiguration fails
                                    onDisconnected(e);
                                } catch (IllegalStateException e) {//TODO: work around old smack
                                    onDisconnected(e);
                                } catch (PushException e) {
                                    onDisconnected(e);
                                } finally {
                                    finishConnectingThread();
                                }
                            }
                        }.start();
                        break;
                    case CONNECTING:
                    case LOADING:
                    case DISCONNECTING:
                        // ignore all other cases
                        break;
                }
                break;
            case DISCONNECTED:
                // spawn thread to do disconnect
                if (mState == ConnectionState.ONLINE) {
                    // update state before starting thread to prevent race conditions
                    updateConnectionState(ConnectionState.DISCONNECTING);

                    new Thread() {
                        public void run() {
                            updateConnectingThread(this);
                            mXMPPConnection.shutdown();
                            onDisconnected("forced disconnect completed");
                            finishConnectingThread();
                            //updateConnectionState(ConnectionState.OFFLINE);
                        }
                    }.start();
                }
                break;
            case OFFLINE:
                switch (mState) {
                    case CONNECTING:
                    case LOADING:
                    case ONLINE:
                        // update state before starting thread to prevent race conditions
                        updateConnectionState(ConnectionState.DISCONNECTING);

                        // spawn thread to do disconnect
                        new Thread() {
                            public void run() {
                                updateConnectingThread(this);
                                mXMPPConnection.shutdown();
                                // we should reset XMPPConnection the next time
                                finishConnectingThread();
                                // reconnect if it was requested in the meantime
                                if (mRequestedState == ConnectionState.ONLINE)
                                    requestConnectionState(ConnectionState.ONLINE);
                            }
                        }.start();
                        break;
                    case DISCONNECTING:
                        break;
                    case DISCONNECTED:
                    case RECONNECT_DELAYED:
                        // TODO: clear timer
                    case RECONNECT_NETWORK:
                        updateConnectionState(ConnectionState.OFFLINE);
                }
                break;
            case RECONNECT_NETWORK:
            case RECONNECT_DELAYED:
                switch (mState) {
                    case DISCONNECTED:
                    case RECONNECT_NETWORK:
                    case RECONNECT_DELAYED:
                        updateConnectionState(new_state);
                        break;
                    default:
                        throw new IllegalArgumentException("Can not go from " + mState + " to " + new_state);
                }
        }
    }

    public void requestConnectionState(ConnectionState new_state) {
        this.doRequestConnectionState(new_state);
    }

    public ConnectionState getConnectionState() {
        return this.mState;
    }

    private synchronized void updateConnectionState(ConnectionState new_state) {
        if (new_state == ConnectionState.ONLINE || new_state == ConnectionState.CONNECTING) {
            this.mLastError = null;
        }

        if (new_state != this.mState) {
            this.mState = new_state;
            if (this.mServiceCallBack != null) {
                this.mServiceCallBack.connectionChanged(this.getConnectionState(), this.mLastError);
            }

        }
    }

    private void onDisconnected(String reason) {
        this.unregisterPongListener();
        this.mLastError = reason;
        this.updateConnectionState(ConnectionState.DISCONNECTED);
    }

    private void onDisconnected(Throwable reason) {
        reason.printStackTrace();

        while (reason.getCause() != null) {
            reason = reason.getCause();
        }

        this.onDisconnected(reason.getLocalizedMessage());
    }

    private void tryToConnect() throws PushException {
        try {
            if (this.mXMPPConnection.isConnected()) {
                try {
                    this.mXMPPConnection.shutdown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            SmackConfiguration.setKeepAliveInterval(0);
            this.mXMPPConnection.connect();
            if (!this.mXMPPConnection.isConnected()) {
                throw new PushException("SMACK connect failed without exception!");
            } else {
                if (this.mConnectionListener != null) {
                    this.mXMPPConnection.removeConnectionListener(this.mConnectionListener);
                }

                this.mConnectionListener = new ConnectionListener() {
                    public void connectionClosedOnError(Exception e) {
                        onDisconnected((Throwable) e);
                        updateConnectionState(ConnectionState.DISCONNECTED);
                    }

                    public void connectionClosed() {
                        updateConnectionState(ConnectionState.OFFLINE);
                    }

                    public void reconnectingIn(int seconds) {
                    }

                    public void reconnectionFailed(Exception e) {
                    }

                    public void reconnectionSuccessful() {
                    }
                };
                this.mXMPPConnection.addConnectionListener(this.mConnectionListener);
                if (!this.mXMPPConnection.isAuthenticated()) {
                    this.mXMPPConnection.login(this.mConfig.user, this.mConfig.pass, this.mConfig.resource);
                }
                this.setStatusFromConfig(Presence.Mode.available);
            }
        } catch (XMPPException e) {
            throw new PushException(e.getLocalizedMessage(), e.getCause());
        } catch (Exception e) {
            e.printStackTrace();
            throw new PushException(e.getLocalizedMessage(), e.getCause());
        }
    }

    private void registerPongListener() {
        this.mPingAlarmPendIntent = PendingIntent.getBroadcast(this.mService.getApplicationContext(), 0, mPingAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        ((AlarmManager) this.mService.getSystemService(Context.ALARM_SERVICE)).setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 30000L, 30000L, this.mPingAlarmPendIntent);
    }

    private void unregisterPongListener() {
        ((AlarmManager) this.mService.getSystemService(Context.ALARM_SERVICE)).cancel(this.mPingAlarmPendIntent);
    }

    private void registerMessageListener() {
        if (this.mPacketListener != null) {
            this.mXMPPConnection.removePacketListener(this.mPacketListener);
        }

        PacketTypeFilter filter = new PacketTypeFilter(Message.class);
        /*
        * 收到  响应
        * */
        this.mPacketListener = new PacketListener() {
            public void processPacket(Packet packet) {
                try {
                    Log.e("mPacketListener",""+packet.toXML());
                    if (packet instanceof Message) {
                        if (((Message) packet).getType() == Message.Type.chat){
                            packet.setFrom(XmppHelper.getUser(packet.getFrom()));
                            packet.setTo(XmppHelper.getUser(packet.getTo()));
                            Message e = (Message) packet;
                            mServiceCallBack.receivedMessage(new PushMessage(e));
                            responseToServer(e);
                        }
                    }else if (packet instanceof IqMessage) {

                        packet.setFrom(XmppHelper.getUser(packet.getFrom()));
                        packet.setTo(XmppHelper.getUser(packet.getTo()));
                        IqMessage e = (IqMessage) packet;
                        mServiceCallBack.receivedIqMessage(e.toXML());
                        responseIqToServer(e);
                    }
                } catch (Exception e) {
                    Log.e("SmackableImpl", "failed to process packet:", e);
                }

            }
        };
        this.mXMPPConnection.addPacketListener(this.mPacketListener, filter);
    }

    private void responseToServer(Message msg) {
//        String fromJID = this.getBareJID(msg.getFrom());
        Message ack = new Message();
        ack.setTo("xlbyun.cn");
        ack.setType(Message.Type.headline);
        ack.addExtension(new MessageResponse(msg.getPacketID()));
        this.mXMPPConnection.sendPacket(ack);
    }
    /*
    * 响应 回复
    * */
    private void responseIqToServer(IqMessage msg) {
//        String fromJID = this.getBareJID(msg.getFrom());
        IqMessage ack = new IqMessage();
        ack.setTo("xlbyun.cn");
        ack.setType(IQ.Type.HEADLINE);
        ack.addExtension(new MessageResponse(msg.getPacketID()));
        this.mXMPPConnection.sendPacket(ack);
    }

    class MessageResponse implements PacketExtension {

        public static final String NAMESPACE = "xlb:ack";
        public static final String ELEMENT = "ack";

        private String id; /// original ID of the delivered message

        public MessageResponse(String id) {
            this.id = id;
        }

        public String getId() {
            return id;
        }

        @Override
        public String getElementName() {
            return ELEMENT;
        }

        @Override
        public String getNamespace() {
            return NAMESPACE;
        }

        @Override
        public String toXML() {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("<ack xmlns=\"");
            stringBuilder.append(NAMESPACE);
            stringBuilder.append("\"><msgid>");
            stringBuilder.append(id);
            stringBuilder.append("</msgid></ack>");
            return stringBuilder.toString();
//            return "<ack xmlns=\"" + NAMESPACE + "\"><msgid>" + id + "</msgid></ack>";
        }

    }


    private void registerPresenceListener() {
        if (this.mPresenceListener != null) {
            this.mXMPPConnection.removePacketListener(this.mPresenceListener);
        }

        this.mPresenceListener = new PacketListener() {
            public void processPacket(Packet packet) {
                try {
                    if(packet instanceof Message){
                        packet.setFrom(XmppHelper.getUser(packet.getFrom()));
                        packet.setTo(XmppHelper.getUser(packet.getTo()));
                        mServiceCallBack.receivedMessage(new PushMessage(packet));
                    }else if(packet instanceof IqMessage){
                        packet.setFrom(XmppHelper.getUser(packet.getFrom()));
                        packet.setTo(XmppHelper.getUser(packet.getTo()));
                        mServiceCallBack.receivedIqMessage(packet.toXML());
                    }
                } catch (Exception e) {
                    Log.e("SmackableImpl", "failed to process presence:");
                    e.printStackTrace();
                }

            }
        };
        this.mXMPPConnection.addPacketListener(this.mPresenceListener, new PacketTypeFilter(Presence.class));
    }


    private void registerIQListener() {
        if (this.mIQListener != null) {
            this.mXMPPConnection.removePacketListener(this.mIQListener);
        }

        this.mIQListener = new PacketListener() {
            public void processPacket(Packet packet) {
                try {
                    if (packet instanceof Message){
                        packet.setFrom(XmppHelper.getUser(packet.getFrom()));
                        packet.setTo(XmppHelper.getUser(packet.getTo()));
                        mServiceCallBack.receivedMessage(new PushMessage(packet));
                    }else if( packet instanceof  IqMessage){
                        packet.setFrom(XmppHelper.getUser(packet.getFrom()));
                        packet.setTo(XmppHelper.getUser(packet.getTo()));
                        mServiceCallBack.receivedIqMessage(packet.toXML());
                    }

                } catch (Exception e) {
                    Log.e("SmackableImpl", "failed to process presence:");
                    e.printStackTrace();
                }

            }
        };
        this.mXMPPConnection.addPacketListener(this.mIQListener, new PacketTypeFilter(IQ.class));
    }


    private void checkToUser(Packet message) {
        String user = XmppHelper.getUserAtHost(message.getTo(), this.mXMPPConnection);
        message.setTo(user);
    }

    public boolean isAuthenticated() {
        return this.mXMPPConnection != null ? this.mXMPPConnection.isConnected() && this.mXMPPConnection.isAuthenticated() : false;
    }

    public void registerCallback(PushListener.PushCallback callBack) {
        this.mServiceCallBack = callBack;
        this.mService.registerReceiver(this.mPingAlarmReceiver, new IntentFilter(PING_ALARM));
    }

    public void unregisterCallback() {
        try {
            this.mXMPPConnection.removePacketListener(this.mPacketListener);
            this.mXMPPConnection.removePacketListener(this.mPresenceListener);
            this.mXMPPConnection.removePacketListener(this.mIQListener);
            this.unregisterPongListener();
        } catch (Exception e) {
            e.printStackTrace();
        }
        this.mRequestedState = ConnectionState.OFFLINE;
        this.requestConnectionState(ConnectionState.OFFLINE);
        this.mService.unregisterReceiver(this.mPingAlarmReceiver);
        this.mServiceCallBack = null;
    }

    public void sendServerPing() {
        if (this.mXMPPConnection != null && this.mXMPPConnection.isAuthenticated()) {
            Ping ping = new Ping();
            ping.setType(org.jivesoftware.smack.packet.IQ.Type.GET);
            ping.setTo(this.mConfig.server);
            mPingID = ping.getPacketID();
            Log.e("SmackableImpl", "Ping: sending ping " + this.mPingID);
            mXMPPConnection.sendPacket(ping);
        } else {
            Log.i("SmackableImpl", "Ping: requested, but not connected to server.");
            requestConnectionState(ConnectionState.ONLINE);
        }
    }

    public String getLastError() {
        return this.mLastError;
    }

    public void sendMessage(Message message) {
        this.checkToUser(message);
//        message.addExtension(new DeliveryReceiptRequest());
        this.mXMPPConnection.sendPacket(message);
    }


    public void sendIqMessage(String message) {
        if(message!=null){
            IqMessage msg=new IqMessage();
            msg.setXmlns("jabber:client");
            msg.setPacketID("idididid");
            msg.setType(IQ.Type.SET);

            QrcodeExt ext =new QrcodeExt();
            ext.setToken("token");
            ext.setQrCode("qrcode");
            msg.addExtension(ext);

            this.mXMPPConnection.sendPacket(msg);
        }
    }

    public void sendPresence(Presence presence) {
        this.checkToUser(presence);
        this.mXMPPConnection.sendPacket(presence);
    }

    private class PingAlarmReceiver extends BroadcastReceiver {
        private PingAlarmReceiver() {
        }

        public void onReceive(Context ctx, Intent i) {
            SmackableImp.this.sendServerPing();
        }
    }

    private class TimeoutCallback implements Runnable {
        private TimeoutCallback() {

        }

        public void run() {
            Log.i("SmackableImpl", "Timeout shutdown!");
            if (mXMPPConnection != null && !mXMPPConnection.isConnected()) {
                requestConnectionState(ConnectionState.DISCONNECTED);
                onDisconnected("Connection timed out");
            }
        }
    }
}
